package com.osdp.common.util;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.swagger.annotations.ApiModelProperty;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.BeanUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyDescriptor;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

public class BeansUtil {
    private static final Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(50000).expireAfterWrite(60, TimeUnit.MINUTES).build();

    //===============================================reflect============================================================
    public static Object getFieldValue(Object data, String fieldName) {
        try {
            if (data == null) {
                return null;
            }
            PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(data.getClass(), fieldName);
            if (pd == null) {
                return null;
            }
            return pd.getReadMethod().invoke(data, new Object[0]);
        } catch (Exception e) {
        }
        return null;
    }

    public static Object getValue(int month, Object entity, String fieldPrefix) {
        if (entity == null) {
            return null;
        }
        try {
            PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(entity.getClass(), fieldPrefix + month);
            return pd.getReadMethod().invoke(entity, new Object[0]);
        } catch (Exception e) {
        }
        return null;
    }

    public static void setValue(int month, Object entity, String fieldPrefix, Object value) {
        try {
            PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(entity.getClass(), fieldPrefix + month);
            pd.getWriteMethod().invoke(entity, value);
        } catch (Exception e) {
        }
    }

    //===============================================date==============================================================

    /**
     * 返回指定的月份6位及年份4位，如202010,2020
     *
     * @param date
     * @return left：月份6位，right：年份4位
     */
    public static Pair<Integer, Integer> monthsAndYears(Date date) {
        date = date == null ? new Date() : date;
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        //月份开始下标为0，所以cal.get(Calendar.MONTH)需要+1才能得到真正月份
        return Pair.of(cal.get(Calendar.YEAR) * 100 + cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR));
    }

    /**
     * 返回指定的月份的上一个月份6位及年份4位，如202010,2020
     *
     * @param date
     * @return left：月份6位，right：年份4位
     */
    public static Pair<Integer, Integer> lastMonthsAndYears(Date date) {
        Pair<Integer, Integer> monthsAndYears = monthsAndYears(date);
        if (monthsAndYears.getLeft() % 100 == 1) {
            return Pair.of((monthsAndYears.getRight() - 1) * 100 + 12, monthsAndYears.getRight() - 1);
        }
        return Pair.of(monthsAndYears.getLeft() - 1, monthsAndYears.getRight());
    }

    /**
     * 返回指定月份的起始及结束时间
     *
     * @param date
     * @return left：月份起始时间，right：月份结束时间
     */
    public static Pair<Date, Date> monthStartAndEnd(Date date) {
        date = date == null ? new Date() : date;
        Date start = DateUtils.truncate(date, Calendar.MONTH);
        Date end = DateUtils.addMonths(start, 1);
        end = DateUtils.addMilliseconds(end, -1);
        return Pair.of(start, end);
    }

    /**
     * 返回指定日期的起始及结束时间
     *
     * @param date
     * @return left：日期起始时间，right：日期结束时间
     */
    public static Pair<Date, Date> dayStartAndEnd(Date date) {
        Date start = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
        Date end = DateUtils.addDays(start, 1);
        end = DateUtils.addMilliseconds(end, -1);
        return Pair.of(start, end);
    }

    /**
     * 返回指定月份的上一个月份的起始及结束时间
     *
     * @param date
     * @return left：月份起始时间，right：月份结束时间
     */
    public static Pair<Date, Date> lastMonthStartAndEnd(Date date) {
        Pair<Date, Date> monthStartAndEnd = monthStartAndEnd(date);
        return Pair.of(DateUtils.addMonths(monthStartAndEnd.getLeft(), -1), DateUtils.addMonths(monthStartAndEnd.getRight(), -1));
    }

    /**
     * 返回指定年份的起始及结束时间
     *
     * @param date
     * @return left：年份起始时间，right：年份结束时间
     */
    public static Pair<Date, Date> yearStartAndEnd(Date date) {
        date = date == null ? new Date() : date;
        Date yearStart = DateUtils.truncate(date, Calendar.YEAR);
        Date yearEnd = DateUtils.addYears(yearStart, 1);
        yearEnd = DateUtils.addMilliseconds(yearEnd, -1);
        return Pair.of(yearStart, yearEnd);
    }

    /**
     * 返回指定年份的上一个年份的起始及结束时间
     *
     * @param date
     * @return left：年份起始时间，right：年份结束时间
     */
    public static Pair<Date, Date> lastYearStartAndEnd(Date date) {
        Pair<Date, Date> yearStartAndEnd = yearStartAndEnd(date);
        return Pair.of(DateUtils.addYears(yearStartAndEnd.getLeft(), -1), DateUtils.addYears(yearStartAndEnd.getRight(), -1));
    }

    public static Date monthsToDate(int months) {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.DAY_OF_MONTH, 1);
        //月份开始下标为0，所以cal.get(Calendar.MONTH)需要+1才能得到真正月份
        cal.set(Calendar.MONTH, months % 100 - 1);
        cal.set(Calendar.YEAR, months / 100);
        return DateUtils.truncate(cal.getTime(), Calendar.DAY_OF_MONTH);
    }

    public static int lastMonths(int months) {
        if (months % 100 == 1) {
            return (months / 100 - 1) * 100 + 12;
        }
        return months - 1;
    }

    public static int nextMonths(int months) {
        if (months % 100 == 12) {
            return (months / 100 + 1) * 100 + 1;
        }
        return months + 1;
    }

    public static int endMonths(int months) {
        return (months / 100) * 100 + 12;
    }

    public static Integer getYears(Integer year) {
        return year != null ? year : BeansUtil.monthsAndYears(new Date()).getRight();
    }

    public static Integer getMonths(Integer year, Integer month) {
        if (month == null) {
            return null;
        }
        if (year != null) {
            return year * 100 + month;
        }
        return BeansUtil.monthsAndYears(new Date()).getRight() * 100 + month;
    }

    public static boolean beforeMonths(int months, Date date) {
        if (date == null) {
            return false;
        }
        return date.getTime() < monthsToDate(months).getTime();
    }

    //===============================================encrypt============================================================
    public static long encrypt(long value, long dataKey) {
        return value ^ dataKey;
    }

    public static long encryptRealValue(BigDecimal value, long dataKey) {
        return (value == null ? 0 : value.multiply(BigDecimal.valueOf(100)).longValue()) ^ dataKey;
    }

    public static long decrypt(long value, long dataKey) {
        return value ^ dataKey;
    }

    public static BigDecimal decryptToRealValue(long value, long dataKey) {
        long decrypt = decrypt(value, dataKey);
        return BigDecimal.valueOf(decrypt).divide(BigDecimal.valueOf(100));
    }

    //===============================================Math===============================================================
    public static BigDecimal toBigDecimal(Number number) {
        return number == null ? BigDecimal.ZERO
                : (number instanceof BigDecimal ? (BigDecimal) number
                : BigDecimal.valueOf(number.doubleValue()));
    }

    public static BigDecimal toBigDecimal(Number number, int scale) {
        BigDecimal value = number == null ? BigDecimal.ZERO
                : (number instanceof BigDecimal ? (BigDecimal) number
                : BigDecimal.valueOf(number.doubleValue()));
        return value.setScale(scale, RoundingMode.DOWN);
    }

    public static BigDecimal add(Number... values) {
        BigDecimal total = BigDecimal.ZERO;
        if (values == null || values.length == 1) {
            return total;
        }
        for (Number value : values) {
            total = total.add(toBigDecimal(value));
        }
        return total;
    }

    public static BigDecimal subtract(Number a, Number b) {
        return toBigDecimal(a).subtract(toBigDecimal(b));
    }

    public static BigDecimal multiply(Number a, Number b) {
        return toBigDecimal(a).multiply(toBigDecimal(b));
    }

    public static BigDecimal multiply(Number a, Number b, int scale) {
        return toBigDecimal(a, scale).multiply(toBigDecimal(b, scale)).setScale(4, RoundingMode.DOWN);
    }

    public static BigDecimal divide(Number a, Number b) {
        return divide(a, b, 2);
    }

    public static BigDecimal divide(Number a, Number b, int scale) {
        BigDecimal bb = toBigDecimal(b);
        return bb.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : toBigDecimal(a).divide(bb, scale, RoundingMode.DOWN);
    }

    public static BigDecimal sum(List<Number> amounts) {
        if (amounts == null || amounts.isEmpty()) {
            return BigDecimal.ZERO;
        }
        BigDecimal total = BigDecimal.ZERO;
        for (Number amount : amounts) {
            total = add(total, amount);
        }
        return total;
    }

    public static <T> BigDecimal sum(List<T> list, Function<T, Number> value) {
        BigDecimal[] total = new BigDecimal[]{BigDecimal.ZERO};
        nullToEmptyList(list).forEach(t -> total[0] = add(total[0], value.apply(t)));
        return total[0];
    }

    public static <T> BigDecimal avg(List<T> list, Function<T, Number> value) {
        BigDecimal[] total = new BigDecimal[]{BigDecimal.ZERO};
        nullToEmptyList(list).forEach(t -> total[0] = add(total[0], value.apply(t)));
        return nullToEmptyList(list).size() > 0 ? BeansUtil.divide(total[0], list.size()) : BeansUtil.toBigDecimal(0);
    }

    public static boolean isAllZero(Object entity, String fieldPrefix) {
        try {
            for (int i = 1; i <= 12; i++) {
                Object value = getValue(i, entity, fieldPrefix);
                if (value == null) {
                    continue;
                }
                if (value instanceof Number) {
                    if (value instanceof BigDecimal) {
                        if (BigDecimal.ZERO.compareTo((BigDecimal) value) != 0) {
                            return false;
                        }
                    } else if (((Number) value).doubleValue() != 0) {
                        return false;
                    }
                }
            }
        } catch (Exception e) {
        }
        return true;
    }

    //===============================================Cache==============================================================
    public static void cache(String key, Object value) {
        if (value == null) {
            cache.invalidate(key);
            return;
        }
        cache.put(key, value);
    }

    public static <T> T cache(String key) {
        return (T) cache.getIfPresent(key);
    }

    public static void clearCache() {
        cache.invalidateAll();
    }

    //===============================================Util==============================================================
    public static <T> List<T> nullToEmptyList(List<T> list) {
        return list == null ? new ArrayList<>() : list;
    }

    public static <T> Set<T> nullToEmptySet(Set<T> set) {
        return set == null ? new HashSet<>() : set;
    }

    public static <K, V> Map<K, V> nullToEmptyMap(Map<K, V> map) {
        return map == null ? Collections.emptyMap() : map;
    }

    public static <T, R> List<R> collect(List<T> list, Function<T, R> function) {
        return nullToEmptyList(list).stream().map(function).collect(Collectors.toList());
    }

    public static <T, R> List<R> collectNotNull(List<T> list, Function<T, R> function) {
        return nullToEmptyList(list).stream().map(function).filter(item -> item != null).collect(Collectors.toList());
    }

    public static <T, R> Map<R, T> collectMap(List<T> list, Function<T, R> function) {
        return nullToEmptyList(list).stream().collect(Collectors.toMap(function, v -> v, (v1, v2) -> v2, LinkedHashMap::new));
    }

    public static <T, R> Map<R, List<T>> collectMapList(List<T> list, Function<T, R> function) {
        return nullToEmptyList(list).stream().collect(Collectors.groupingBy(function, LinkedHashMap::new, Collectors.toList()));
    }

    public static <T> String join(List<T> list, Function<T, String> function) {
        return nullToEmptyList(list).stream().map(function).collect(Collectors.joining(","));
    }

    public static boolean isEmpty(Object obj) {
        if (obj == null) {
            return true;
        }
        if (obj instanceof CharSequence) {
            return StringUtils.isBlank((CharSequence) obj);
        } else if (obj instanceof Collection) {
            return ((Collection) obj).size() < 1;
        } else if (obj instanceof Map) {
            return ((Map) obj).size() < 1;
        } else if (obj.getClass().isArray()) {
            return Array.getLength(obj) < 1;
        }
        return false;
    }

    public static boolean allEmpty(Object... objs) {
        if (objs == null || objs.length < 1) {
            return true;
        }
        for (Object obj : objs) {
            if (obj == null) {
                continue;
            }
            if (obj instanceof CharSequence) {
                if (!StringUtils.isBlank((CharSequence) obj)) {
                    return false;
                }
            } else if (obj instanceof Collection) {
                if (((Collection) obj).size() > 0) {
                    return false;
                }
            } else if (obj instanceof Map) {
                if (((Map) obj).size() > 0) {
                    return false;
                }
            } else if (obj.getClass().isArray()) {
                if (Array.getLength(obj) > 0) {
                    return false;
                }
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * 对象值复制，使用Spring BeanUtils
     * @param src 源对象
     * @param target  目标对象
     * @param <T>
     * @return
     */
    public static <T> T copy(Object src, T target) {
        if (src == null) {
            return target;
        }
        if (target == null) {
            return null;
        }
        BeanUtils.copyProperties(src, target);
        return target;
    }

    public static Map<?, ?> toMap(Object... keyValues) {
        Map map = new LinkedHashMap();
        if (keyValues == null || keyValues.length < 1 || keyValues.length % 2 != 0) {
            return map;
        }
        for (int i = 0; i < keyValues.length; i++) {
            map.put(keyValues[i], keyValues[i + 1]);
            i = i + 1;
        }
        return map;
    }

    public static <T> List<T> toList(T... values) {
        List<T> list = new ArrayList<>();
        if (values == null || values.length < 1) {
            return list;
        }
        for (T value : values) {
            list.add(value);
        }
        return list;
    }

    public static <T> List<T> toListIgnoreNull(T... values) {
        List<T> list = new ArrayList<>();
        if (values == null || values.length < 1) {
            return list;
        }
        for (T value : values) {
            if (value != null) {
                list.add(value);
            }
        }
        return list;
    }

    public static void download(String fileName, InputStream is, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String userAgent = StringUtils.defaultString(request.getHeader("User-Agent"));
        boolean isIE = StringUtils.lowerCase(userAgent).contains("msie");
        // 先去掉文件名称中的空格,然后转换编码格式为utf-8,保证不出现乱码,这个文件名称用于浏览器的下载框中自动显示的文件名
        fileName = StringUtils.replace(fileName, " ", "");
        String attachmentFileName = isIE
                ? URLEncoder.encode(fileName, "UTF-8")
                : new String(fileName.getBytes("UTF-8"), "ISO8859-1");
        response.reset();
        response.setStatus(200);
        response.addHeader("Content-Disposition", "attachment;filename=" + attachmentFileName);
        //response.addHeader("Content-Length", "" + size);
        response.setContentType("application/octet-stream");
        IOUtils.copyLarge(is, response.getOutputStream());
        response.getOutputStream().flush();
    }

    public static String mapToParamString(Map<?, ?> map) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            sb.append(encodeUrl(entry.getKey())).append("=").append(entry.getValue() == null ? "" : encodeUrl(entry.getValue())).append("&");
        }
        if (sb.length() > 0) {
            sb.setLength(sb.length() - 1);
        }
        return sb.toString();
    }

    public static Map<String/*field*/, String/*name*/> getFieldNameMap(Class<?> cls) {
        if (cls == null) {
            return Collections.emptyMap();
        }
        Map<String, ApiModelProperty> map = new HashMap<>();
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
            if (annotation == null) {
                continue;
            }
            map.put(field.getName(), annotation);
        }
        LinkedHashMap<String, String> result = map.entrySet().stream()
                .sorted(Comparator.comparingInt(v -> v.getValue().position()))
                .collect(Collectors.toMap(v -> v.getKey(), v -> v.getValue().value(), (v1, v2) -> v2, LinkedHashMap::new));
        return result;
    }

//    public static Map<String/*field*/, FRDrillPropDTO/*字段配置*/> getDrillPropMap(Class<?> cls) {
//        if (cls == null) {
//            return new LinkedHashMap<>();
//        }
//        Map<String, ApiModelProperty> map = new HashMap<>();
//        Map<String, Boolean> numberMap = new HashMap<>();
//        Map<String, Boolean> dateMap = new HashMap<>();
//        Field[] fields = cls.getDeclaredFields();
//        for (Field field : fields) {
//            ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
//            if (annotation == null) {
//                continue;
//            }
//            map.put(field.getName(), annotation);
//            numberMap.put(field.getName(), field.getType() == BigDecimal.class);
//            dateMap.put(field.getName(), java.util.Date.class.isAssignableFrom(field.getType()));
//        }
//        LinkedHashMap<String/*field*/, FRDrillPropDTO/*字段配置*/> result = map.entrySet().stream()
//                .sorted(Comparator.comparingInt(v -> v.getValue().position()))
//                .collect(Collectors.toMap(v -> v.getKey(), v -> FRDrillPropDTO.builder()
//                        .name(v.getValue().value())
//                        .field(v.getKey())
//                        .number(Boolean.TRUE.equals(numberMap.get(v.getKey())))
//                        .date(Boolean.TRUE.equals(dateMap.get(v.getKey())))
//                        .build(), (v1, v2) -> v2, LinkedHashMap::new));
//        return result;
//    }

//    public static Pair<Excels.ExcelConfigData, List<Excels.CellData>> getExcelConfig(Map<String/*field*/, FRDrillPropDTO/*字段配置*/> props) {
//        Excels.ExcelConfigData excelConfigData = Excels.ExcelConfigData.builder()
//                .styles(Arrays.asList(
//                        Excels.StyleData.builder().name("style1").fontFamily("微软雅黑").fontSize((short) 10).build(),
//                        Excels.StyleData.builder().name("style2").fontFamily("微软雅黑").fontSize((short) 10).dataFormat("#,##0.00_);[Red](#,##0.00)").build(),
//                        Excels.StyleData.builder().name("style3").fontFamily("微软雅黑").fontSize((short) 10).dataFormat("yyyy-mm-dd;@").build(),
//                        Excels.StyleData.builder().name("header1").bgColor(IndexedColors.LIGHT_TURQUOISE).fontFamily("微软雅黑").fontSize((short) 10).dataFormat("#,##0.00_);[Red](#,##0.00)").build()
//                ))
//                .build();
//        List<Excels.CellData> list = new ArrayList<>();
//        int count = 1;
//        for (FRDrillPropDTO prop : props.values()) {
//            list.add(Excels.CellData.builder()
//                    .name(prop.getName())
//                    .order(count++)
//                    .styleName(prop.isNumber() ? "style2" : (prop.isDate() ? "style3" : "style1"))
//                    .headerStyleName("header1")
//                    .dataFieldName(prop.getField())
//                    .width(prop.isNumber() ? 10 : (prop.isDate() ? 10 : 20))
//                    .build());
//        }
//        return Pair.of(excelConfigData, list);
//    }

    static String encodeUrl(Object value) {
        try {
            return URLEncoder.encode(String.valueOf(value), "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(value);
    }

//    public static <T> Page<T> toPage(int pageNo, int pageSize, String sortName, String sortOrder) {
//        Page<T> pagination = new Page<>(pageNo, pageSize);
//        if (StringUtils.isNotBlank(sortName)) {
//            QueryOrderEnum order = QueryOrderEnum.from(sortOrder);
//
//            if (order == null || order == QueryOrderEnum.ASC) {
//                pagination.addOrder(OrderItem.asc(sortName));
////                pagination.setAsc(sortName);
//            } else {
//                pagination.addOrder(OrderItem.desc(sortName));
////                pagination.setDesc(sortName);
//            }
//        }
//        return pagination;
//    }
}
